package mn.more.wits.client.dto;

import com.google.gwt.user.client.rpc.IsSerializable;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Base class for all GWT model objects.  Extending from this class is required
 * in order to unuformly support all browser-server encyption.
 * <p/>
 * To sufficiently support the client-server and server-client encryption,
 * <code>BaseModel</code> stores all data elements in an internal {@link Map} as
 * <b>string-only value pair</b>.  During encryption, these name-value value
 * pair will be combined into a string and then be encrypted.  During
 * decryption, the decrypted string will be parsed back into the original
 * name-value pair.
 * <p/>
 * In order to support complex data types and intra-object relationships,
 * <code>BaseModel</code> defines 2 methods that must be implemented by its
 * subclasses: {@link #beforeSerialize()} and {@link #afterDeserialize()}. The
 * {@link #beforeSerialize()} method is called within the {@link #serialize()}
 * method PRIOR to any actual serialization (to string). Similarly, the {@link
 * * #afterDeserialize()} method is called within the {@link #deserialize(String)}
 * method AFTER all deserialization (to properties). In this way, subclasses are
 * given "hooks" to further modify or enhance the encryption/decryption process,
 * if need be.
 *
 * @author <a href="mailto:mike.liu@aptechmongolia.edu.mn">Mike Liu</a>
 * @version $Id: DataTransferObject.java 5 2008-09-01 12:08:42Z mikeliucc $
 * @see Payload
 */
public abstract class DataTransferObject
	implements IsSerializable, Serializable {

	/** separator between each field */
	protected static final char FIELD_DELIM = (char) 0x08;
	protected static final String FIELD_DELIM_STR = new String(new char[]{FIELD_DELIM});
	/** separator between each name-value pair */
	protected static final char PAIR_DELIM = (char) 0x09;
	protected static final String PAIR_DELIM_STR = new String(new char[]{PAIR_DELIM});
	/** separator between each index of an array (value) */
	protected static final char ARRAY_DELIM = (char) 0x0B;
	protected static final String ARRAY_DELIM_STR = new String(new char[]{ARRAY_DELIM});

	/** separator to signified an inner nested object. */
	protected static final char BM_DELIM = (char) 0x0C;
	/** separator between each field of the inner nested object */
	protected static final char BM_FIELD_DELIM = (char) 0x0E;
	protected static final String BM_FIELD_DELIM_STR = new String(new char[]{BM_FIELD_DELIM});
	/** separator between each name-value pair of the inner nested object */
	protected static final char BM_PAIR_DELIM = (char) 0x0F;
	protected static final String BM_PAIR_DELIM_STR = new String(new char[]{BM_PAIR_DELIM});
	/** separator between each index of an array (value) of the inner nested object */
	protected static final char BM_ARRAY_DELIM = (char) 0x1E;
	protected static final String BM_ARRAY_DELIM_STR = new String(new char[]{BM_ARRAY_DELIM});

	/** special value to indicate that an int property does not exist */
	protected static final int INT_NULL = Integer.MIN_VALUE;
	/**
	 * special value to indicate that an invalid string value which cannot be
	 * converted into a int.  We used this technique so that we don't have to deal
	 * with exception handling on the client-side.
	 */
	protected static final int INT_CONVERSION_ERROR = Integer.MIN_VALUE + 2;
	/** special value to indicate that an long property does not exist */
	protected static final long LONG_NULL = Long.MIN_VALUE;
	/**
	 * special value to indicate that an invalid string value which cannot be
	 * converted into a long.  We used this technique so that we don't have to deal
	 * with exception handling on the client-side.
	 */
	protected static final long LONG_CONVERSION_ERROR = Long.MIN_VALUE + 2;
	/**
	 * special value to indicate that an boolean property does not exist. Note that
	 * this is inheritly problematic.
	 */
	protected static final boolean BOOL_NULL = false;

	// map of string-string
	/** @gwt.typeArgs <java.lang.String> */
	protected Map data;

	public DataTransferObject() {
		// data is initialized here so that subclass has the option to
		// override the default behavior.  For examlple, subclass can choose to
		// initialize the model with some pre-defined data, or a different
		// sizing option.
		data = new HashMap();
	}

	/**
	 * called within the {@link #serialize()} method before the actual
	 * "serialization" starts.  This gives subclasses a chance to prepare the data
	 * in the internal map storage, if need be.
	 */
	protected abstract void beforeSerialize();

	/**
	 * "serialize" data from the internal map storage to a string.  In this method,
	 * the <code>preStringify()</code> is called so that subclasses can further
	 * manipulate the data in the internal map storage.  After that, this method
	 * "converts" all data within the map into a <b>string-only name-value pair
	 * </b>.
	 */
	public String serialize() {
		beforeSerialize();

		StringBuffer buffer = new StringBuffer("01");
		buffer.append(FIELD_DELIM);

		Object[] keys = data.keySet().toArray();
		for (int i = 0; i < keys.length; i++) {
			String key = (String) keys[i];
			String value = (String) data.get(key);
			buffer.append(key).append(PAIR_DELIM).append(value).append(FIELD_DELIM);
		}

		return buffer.toString();
	}

	/**
	 * "deserialize" data from the internal map storage to a string.  In this
	 * method, the <code>deserialize()</code> is called so that subclasses can
	 * further manipulate the data in the internal map storage.  After that, this
	 * method "converts" all data within the map into a <b>string-only name-value
	 * pair</b>.
	 */
	public void deserialize(String payload) {
		// split on the FIELD_DELIM (field delimiter)
		String[] pairs = payload.split("\\x08");

		// first pair not used here.  Originally I intended to use it to
		// double-check class compatibility.  However, getClass() is not "yet"
		// supported in GWT 1.4
		String notUsed = pairs[0];
		//if (!notUsed.equals(this.getClass().getName())) {
		//	throw new RuntimeException(
		//		"wrong class name specified from the payload data stream!");
		//} else {

		for (int i = 1; i < pairs.length; i++) {
			String pair = pairs[i];
			int pos = pair.indexOf(PAIR_DELIM);
			String key = pair.substring(0, pos);
			String value = pair.substring(pos + 1);
			data.put(key, value);
		}
		//}

		// sub-class gets the chance to do extra steps to handle non-simple
		// data types like arrays or UDT.
		afterDeserialize();
	}

	/**
	 * called within the {@link #deserialize(String)} method, after the payload has
	 * been "deserialized" into the internal map storage" so that sub-class can
	 * handle the more complex data types.  The {@link #deserialize(String)} method
	 * will set * all data from the payload to the internal map.  It is the
	 * responsibility of each subclass to handle the data individually, if need
	 * be.
	 */
	protected abstract void afterDeserialize();

	/** get data from the internal map storage. */
	protected String getData(String field) { return (String) data.get(field); }

	/** get data from the internal map storage. */
	protected int getIntData(String field) {
		return toInt((String) data.get(field));
	}

	/** get data from the internal map storage. */
	protected long getLongData(String field) {
		return toLong((String) data.get(field));
	}

	/** get data from the internal map storage. */
	protected boolean getBooleanData(String field) {
		//return ((Boolean) data.get(field)).booleanValue();
		return Boolean.valueOf((String) data.get(field)).booleanValue();
	}

	/** get data from the internal map storage. */
	protected Date getDateData(String field) {
		//return (Date) data.get(field);
		return new Date(toLong((String) data.get(field)));
	}

	/** put data into the internal map storage. */
	protected void setData(String field, String value) { data.put(field, value); }

	/** put data into the internal map storage. */
	protected void setData(String field, int value) { data.put(field, value + ""); }

	/** put data into the internal map storage. */
	protected void setData(String field, long value) { data.put(field, value + ""); }

	/** put data into the internal map storage. */
	protected void setData(String field, boolean value) { data.put(field, value + ""); }

	/** put data into the internal map storage. */
	protected void setData(String field, Date value) { data.put(field, value.getTime() + ""); }

	/** put data into the internal map storage. */
	protected void setData(String field, DataTransferObject value) {
		data.put(field, BM_DELIM + toModelString(value) + BM_DELIM);
	}

	/**
	 * convert an {@link DataTransferObject} instance into its string version,
	 * including incrementing its internal object level from <code>"01"</code> to
	 * <code>"02"</code>.  The purpose of this method is to convert an existing
	 * model instance into a serialized form so that it can be properly encrypted
	 * later.
	 *
	 * @see #toBaseModel(String, DataTransferObject)
	 */
	protected static String toModelString(DataTransferObject value) {
		String str = value.serialize();

		// increment level
		String levelStr = str.substring(0, 2);
		int level = Integer.parseInt(levelStr);
		int newLevel = level + 1;
		String newLevelStr = (newLevel < 10) ? "0" + newLevel : newLevel + "";
		str = newLevelStr + str.substring(2);

		// replace delimeter
		str = str.replaceAll(FIELD_DELIM_STR, BM_FIELD_DELIM_STR);
		str = str.replaceAll(PAIR_DELIM_STR, BM_PAIR_DELIM_STR);
		str = str.replaceAll(ARRAY_DELIM_STR, BM_ARRAY_DELIM_STR);
		return str;
	}

	/**
	 * convert the string version of <code>data</code> into its numeric form. Since
	 * this class will be used on the client-side, logging and exception handling
	 * is tricky.  Thus a special <code>INT_CONVERSION_ERROR</code> constants
	 * denotes the presence of a string-to-int conversion error.
	 *
	 * @see #INT_CONVERSION_ERROR
	 */
	protected static int toInt(String data) {
		if (data == null || data.length() < 1) { return INT_NULL; }
		// we need better checking here
		try {
			return Integer.parseInt(data);
		} catch (NumberFormatException e) {
			return INT_CONVERSION_ERROR;
		}
	}

	/**
	 * convert the string version of <code>data</code> into its numeric form. Since
	 * this class will be used on the client-side, logging and exception handling
	 * is tricky.  Thus a special <code>LONG_CONVERSION_ERROR</code> constants
	 * denotes the presence of a string-to-long conversion error.
	 *
	 * @see #LONG_CONVERSION_ERROR
	 */
	protected static long toLong(String data) {
		if (data == null || data.length() < 1) { return LONG_NULL; }
		// todo: we need better checking here
		return Long.parseLong(data);
	}

	/**
	 * convert the string version of <code>data</code> into a BaseModel form. Since
	 * this class will be used on the client-side, logging and exception handling
	 * is tricky.
	 */
	protected static DataTransferObject toBaseModel(String data, DataTransferObject dto) {
		if (data == null || data.length() < 1) { return null; }
		// todo: we need better checking here
		String levelString = data.substring(0, 2);
		int level = 0;
		try {
			level = Integer.parseInt(levelString);
		} catch (NumberFormatException e) {
			// not a number... continue;
		}

		if ((data.charAt(0) == BM_DELIM && data.charAt(data.length() - 1) == BM_DELIM) ||
		    level > 1) {
			data = data.substring(1, data.length() - 1);
			data = data.replaceAll(BM_FIELD_DELIM_STR, FIELD_DELIM_STR);
			data = data.replaceAll(BM_PAIR_DELIM_STR, PAIR_DELIM_STR);
			data = data.replaceAll(BM_ARRAY_DELIM_STR, ARRAY_DELIM_STR);
		}

		dto.deserialize(data);
		return dto;
	}
}